﻿/*
 * Copyright (c) 2015-2016 Constantijn Evenhuis (blueprints@const.nl)
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
using System;
using System.Globalization;

namespace Blueprints.Defaults
{
    internal sealed class DateTimeParser
    {
        private static readonly string[] formats = new string[]
        {
            "yyyy-M-d H:mm:ss",
            "yyyy-M-d H:mm",
            "yyyy-M-d",
            "H:mm:ss",
            "H:mm",
        };

        public static bool TryParse(string str, out DateTime dateTime)
        {
            if (DateTime.TryParseExact(str, formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out dateTime))
            {
                return true;
            }

            StringParser parser = new StringParser(str);
            bool yearNegative = parser.SkipIf('-');
            int yearLength = parser.OffsetTo('-');
            if (yearLength < 4)
            {
                dateTime = default(DateTime);
                return false;
            }

            int year;
            if (!parser.TryReadInt32(yearLength, out year))
            {
                dateTime = default(DateTime);
                return false;
            }

            if (yearNegative)
            {
                year = -year;
            }

            parser.Skip(1);

            int month;
            int day;
            int hour;
            int minute;
            int second;
            if (!parser.TryReadInt32(2, out month)
                || !parser.SkipIf('-')
                || !parser.TryReadInt32(2, out day)
                || !parser.SkipIf('T')
                || !parser.TryReadInt32(2, out hour)
                || !parser.SkipIf(':')
                || !parser.TryReadInt32(2, out minute)
                || !parser.SkipIf(':')
                || !parser.TryReadInt32(2, out second))
            {
                dateTime = default(DateTime);
                return false;
            }

            long ticks;
            if (parser.SkipIf('.'))
            {
                if (!TryParseTicks(parser, out ticks))
                {
                    dateTime = default(DateTime);
                    return false;
                }
            }
            else
            {
                ticks = 0;
            }

            TimeSpan? timezone;
            if (!TryParseTimezone(parser, out timezone))
            {
                dateTime = default(DateTime);
                return false;
            }

            if (timezone.HasValue)
            {
                dateTime = new DateTime(year, month, day, hour, minute, second, DateTimeKind.Utc);
                dateTime = dateTime.Subtract(timezone.Value);
                dateTime = dateTime.ToLocalTime();
            }
            else
            {
                dateTime = new DateTime(year, month, day, hour, minute, second, DateTimeKind.Local);
            }

            dateTime = dateTime.AddTicks(ticks);
            return true;
        }

        public static string ToString(DateTime dateTime)
        {
            switch (dateTime.Kind)
            {
                case DateTimeKind.Utc:
                    return dateTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
                case DateTimeKind.Local:
                case DateTimeKind.Unspecified:
                default:
                    return dateTime.ToString("yyyy-MM-ddTHH:mm:ss.fff");
            }
        }

        private static bool TryParseTicks(StringParser parser, out long ticks)
        {
            int ticksLength = parser.OffsetToAny('Z', '+', '-');
            if (ticksLength < 0)
            {
                ticksLength = parser.Length - parser.Cursor;
            }

            if (ticksLength == 0)
            {
                ticks = default(long);
                return false;
            }

            if (!parser.TryReadInt64(ticksLength, out ticks))
            {
                ticks = default(long);
                return false;
            }

            int power = 7 - ticksLength;
            ticks = (long)(ticks * Math.Pow(10, power));
            return true;
        }

        private static bool TryParseTimezone(StringParser parser, out TimeSpan? timezone)
        {
            char type;
            if (!parser.TryReadChar(out type) || type == 'Z')
            {
                timezone = null;
                return true;
            }

            bool negative;
            if (type == '-')
            {
                negative = true;
            }
            else if (type == '+')
            {
                negative = false;
            }
            else
            {
                timezone = default(TimeSpan?);
                return false;
            }

            int timezoneHour;
            int timezoneMinute;
            if (!parser.TryReadInt32(2, out timezoneHour)
                || !parser.SkipIf(':')
                || !parser.TryReadInt32(2, out timezoneMinute))
            {
                timezone = default(TimeSpan?);
                return false;
            }

            if (parser.Cursor != parser.Length)
            {
                timezone = default(TimeSpan?);
                return false;
            }

            timezone = new TimeSpan(timezoneHour, timezoneMinute, 0);
            if (negative)
            {
                timezone = timezone.Value.Negate();
            }

            return true;
        }

        private sealed class StringParser
        {
            public StringParser(string s)
            {
                String = s;
                Length = s.Length;
                Cursor = 0;
            }

            public string String
            {
                get;
                private set;
            }

            public int Cursor
            {
                get;
                private set;
            }

            public int Length
            {
                get;
                private set;
            }

            public int OffsetTo(char c)
            {
                int offset = String.IndexOf(c, Cursor);
                if (offset < 0)
                {
                    return -1;
                }

                return offset - Cursor;
            }

            public int OffsetToAny(params char[] c)
            {
                int offset = String.IndexOfAny(c, Cursor);
                if (offset < 0)
                {
                    return -1;
                }

                return offset - Cursor;
            }

            public bool SkipIf(char c)
            {
                if (Cursor >= Length)
                {
                    return false;
                }

                if (String[Cursor] != c)
                {
                    return false;
                }

                Cursor++;
                return true;
            }

            public void Skip(int count)
            {
                Cursor += count;
            }

            public bool TryReadInt32(int length, out int value)
            {
                if (Length < Cursor + length)
                {
                    value = default(int);
                    return false;
                }

                bool negative = String[Cursor] == '-';
                if (negative)
                {
                    Cursor++;
                    if (!TryReadInt32(length - 1, out value))
                    {
                        value = default(int);
                        return false;
                    }

                    value = -value;
                    return true;
                }

                value = 0;
                for (int i = 0; i < length; i++)
                {
                    int digit = String[Cursor++] - '0';
                    if (digit < 0 || digit > 9)
                    {
                        value = default(int);
                        return false;
                    }

                    value *= 10;
                    value += digit;
                }

                return true;
            }

            public bool TryReadInt64(int length, out long value)
            {
                if (Length < Cursor + length)
                {
                    value = default(int);
                    return false;
                }

                bool negative = String[Cursor] == '-';
                if (negative)
                {
                    Cursor++;
                    if (!TryReadInt64(length - 1, out value))
                    {
                        value = default(int);
                        return false;
                    }

                    value = -value;
                    return true;
                }

                value = 0;
                for (int i = 0; i < length; i++)
                {
                    int digit = String[Cursor++] - '0';
                    if (digit < 0 || digit > 9)
                    {
                        value = default(int);
                        return false;
                    }

                    value *= 10;
                    value += digit;
                }

                return true;
            }

            public bool TryReadChar(out char c)
            {
                if (Cursor >= Length)
                {
                    c = default(char);
                    return false;
                }

                c = String[Cursor++];
                return true;
            }
        }
    }
}
